/* ---------------------------------------------Marlin Toolkit----------------------------------------*
Authors : Daniel Randle, Alex Bourne
Date Created : Mar 20 2013

File:
nsframebufferobject.cpp

Description:
This file contains the definition for the NSFrameBufferObject class functions along with any
helper functions associated with the class
*-----------------------------------------------------------------------------------------------------*/

#include <nsframebufferobject.h>
#include <nsobjectreference.h>
#include <logfile.h>
#include <nsobjectset.h>
#include <nsobject.h>
#include <nsshader.h>
#include <sstream>
#include <nscamera.h>
#include <nstexture.h>
#include <nsselectionmodel.h>
#include <nsmesh.h>
#include <nsmaparea.h>


NSFrameBufferObject::NSFrameBufferObject(NSObjectSet * _objects, NSMapArea * _mapArea, int width, int height):FBO(0),objects(_objects),mapArea(_mapArea), w(width),h(height)
{
	LogFile("Creating base FBO...");
	bType = Render;
}

NSFrameBufferObject::~NSFrameBufferObject()
{
    if (FBO != 0) 
	{
        glDeleteFramebuffers(1, &FBO);
		LogFile("Successfully deleted FBO");
    }

	LogFile("FBO Deleted");
}

bool NSFrameBufferObject::init(int width, int height)
{
	LogFile("Initializing base FBO...");
	w = width; h = height;
	return true;
}

bool NSFrameBufferObject::resetSize(int width, int height)
{
    if (FBO != 0) 
	{
        glDeleteFramebuffers(1, &FBO);
		FBO = 0;
		LogFile("Reset FBO");
    }
	return init(width,height);
}

void NSFrameBufferObject::draw(const NSMatrix4Df & proj, NSCamera * camera)
{
	//use();
	//disable();
}

void NSFrameBufferObject::use()
{
	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, FBO);
}

void NSFrameBufferObject::disable()
{
	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}

int NSFrameBufferObject::height()
{
	return h;
}

int NSFrameBufferObject::width()
{
	return w;
}

NSGBuffer::NSGBuffer(NSObjectSet * _objects, 
	NSMapArea * mapArea, 
	int width, 
	int height):
	NSFrameBufferObject(_objects, mapArea, width, height), 
	depthTexture(0),
	finalTexture(0),
	shadowTexture(0),
	shadowFBO(0)
{
	LogFile("Creating GBuffer FBO");

	for (int i = 0; i < TexNum; ++i)
		textures[i] = 0;
	
	bType = GBuffer;
	shShader = new NSShadowShader("ShadowShader","shadowmap.fsh","shadowmap.vsh");
	fsQuad = new NSShader("QuadShader","skydome.fsh","skydome.vsh");//,"background.gsh");
	fogS = new NSShader("FogShader","fog.fsh","directionlight.vsh","background.gsh");
	fogS->enable();
	fogS->setUniform("worldPosMap",NSTexture::GWorldPos);
	fogS->setUniform("diffuseMap",NSTexture::GDiffuse);
	quad = new NSMesh();
	quad->loadScene("Meshes/Skybox/skydome.dae");
	tex = new NSTexture(NSTexture::SkyBox);
	//tex->loadCubemapFromFile("front.jpg","back.jpg","bot.jpg","top.jpg","left.jpg","right.jpg","Textures/Skybox/Space",false);
	tex->loadCubemapFromFile("stormydays_ft.tga","stormydays_bk.tga","stormydays_dn.tga","stormydays_up.tga","stormydays_rt.tga","stormydays_lf.tga","Textures/Skybox/Storm",false);
	fsQuad->enable();
	fsQuad->setUniform("skyBox",NSTexture::SkyBox);
}


NSGBuffer::~NSGBuffer()
{
	delete tex;
	delete fsQuad;
	delete quad;
	delete shShader;
	LogFile("Deleting GBuffer FBO");
    if (depthTexture != 0) 
	{
        glDeleteTextures(1, &depthTexture);
		LogFile("Successfully deleted GBuffer depth texture");
    }
    if (finalTexture != 0) 
	{
        glDeleteTextures(1, &finalTexture);
		LogFile("Successfully deleted GBuffer final texture");
    }
    if (shadowTexture != 0)
	{
        glDeleteTextures(1, &shadowTexture);
		LogFile("Successfully deleted shadow depth texture");
    }
    if (shadowFBO != 0)
	{
		glDeleteFramebuffers(1,&shadowFBO);
		LogFile("Successfully deleted shadow FBO");
    }
	for (int i = 0; i < TexNum; ++i)
	{
		if (textures[i] != 0) 
		{
			std::stringstream ss;
			glDeleteTextures(1, &textures[i]);
			ss << "Successfully deleted GBuffer color texture " << i;
			LogFile l(ss);
		}
	}
	LogFile("GBuffer FBO Deleted");
}

void NSGBuffer::_lightPass(const NSMatrix4Df & projCamMat, const NSVec3Df & camPos, const NSVec2Df & screenSize, NSObjectReference * light)
{
	NSObject * base = light->getBase();
	glDrawBuffer(Final);
	glStencilFunc(GL_NOTEQUAL, 0, 0xFF);
	glDisable(GL_DEPTH_TEST);
	glEnable(GL_BLEND);
	glBlendEquation(GL_FUNC_ADD);
	glBlendFunc(GL_ONE, GL_ONE);
	glEnable(GL_CULL_FACE);
	glCullFace(GL_FRONT);
	
	if (base->getObjectType() == NSObject::SpotLight)
		((NSSpotLight*)base)->drawLight(projCamMat,camPos,screenSize,(NSSpotLightReference*)light);
	else
		((NSPointLight*)base)->drawLight(projCamMat,camPos,screenSize,(NSPointLightReference*)light);

	glCullFace(GL_BACK);
	glDisable(GL_STENCIL_TEST);
}

void NSGBuffer::_spotLightShadowPass(NSObjectReference * light)
{
	glDepthMask(GL_TRUE);
	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);
	glClear(GL_DEPTH_BUFFER_BIT);
	glEnable(GL_DEPTH_TEST);
	NSSpotLightReference* spotLight = (NSSpotLightReference*)light;
	NSMapArea::Iterator iter = mapArea->begin();
	while (iter != mapArea->end())
	{
		NSMapArea::ObjectIterator iter2 = (*iter)->begin();
		while (iter2 != (*iter)->end())
		{
			if (iter2->first->isCastShadows())
				iter2->first->draw();
			++iter2;
		}
		++iter;
	}
}

void NSGBuffer::_stencilPass(const NSMatrix4Df & projCamMat, const NSVec3Df & camPos, const NSVec2Df & screenSize, NSObjectReference * light)
{
	NSObject * base = light->getBase();
	glDrawBuffer(GL_NONE);
	glEnable(GL_DEPTH_TEST);
	glDisable(GL_CULL_FACE);
	glClear(GL_STENCIL_BUFFER_BIT);
	glEnable(GL_STENCIL_TEST);
	glStencilFunc(GL_ALWAYS, 0, 0);
	glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR, GL_KEEP);
	glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR, GL_KEEP);

	if (base->getObjectType() == NSObject::SpotLight)
		((NSSpotLight*)base)->drawLight(projCamMat,camPos,screenSize,(NSSpotLightReference*)light);
	else
		((NSPointLight*)base)->drawLight(projCamMat,camPos,screenSize,(NSPointLightReference*)light);
}

void NSGBuffer::blendPointLights(const NSMatrix4Df & projCamMat, const NSVec3Df & camPos, const NSVec2Df & screenSize)
{
	NSMapArea::ObjectIterator pointLightIter = mapArea->objectsBegin(NSObject::PointLight);
	NSShader * lShader = NULL;
	while (pointLightIter != mapArea->objectsEnd(NSObject::PointLight))
	{
		NSPointLight * currentLight = (NSPointLight*)pointLightIter->first;
		NSShader * tstShader = currentLight->getLightShader();

		if (lShader != tstShader)
		{
			lShader = tstShader;
			lShader->enable();
		}

		NSMapArea::RefIterator pointLightRefIter = pointLightIter->second.begin();
		while (pointLightRefIter != pointLightIter->second.end())
		{
			_stencilPass(projCamMat,camPos,screenSize,*pointLightRefIter);
			_lightPass(projCamMat,camPos,screenSize,*pointLightRefIter);
			++pointLightRefIter;
		}
		++pointLightIter;
	}
	lShader->disable();
}

void NSGBuffer::blendSpotLights(const NSMatrix4Df & projCamMat, const NSVec3Df & camPos, const NSVec2Df & screenSize, const NSMatrix4Df & proj, NSCamera* cam)
{
	NSMapArea::ObjectIterator spotLightIter = mapArea->objectsBegin(NSObject::SpotLight);
	NSShader * lShader = NULL;
	while (spotLightIter != mapArea->objectsEnd(NSObject::SpotLight))
	{
		NSSpotLight * currentLight = (NSSpotLight*)spotLightIter->first;
		NSShader * tstShader = currentLight->getLightShader();
		NSMatrix4Df proje = NSMatrix4Df::getProjectionMatrix(currentLight->getCutoff()+60.0f,2048,2048,1,1000);

		// Only switch shader if the light shader is different from the last one
		if (lShader != tstShader)
		{
			lShader = tstShader;
			lShader->enable();
		}
		NSMapArea::RefIterator spotLightRefIter = spotLightIter->second.begin();
		while (spotLightRefIter != spotLightIter->second.end())
		{
			NSMatrix4Df mat(proje * (*spotLightRefIter)->getPOVTransform());
			glViewport(0,0,2048,2048);
			shShader->enable();
			shShader->setUniform("projLightMat",mat);
			useShadow();
			_spotLightShadowPass(*spotLightRefIter);
			glViewport(0,0,w,h);
			
			lShader->enable();
			use();
			enableShadowTexture();
			lShader->setUniform("projCamMat",projCamMat);
			lShader->setUniform("projLightMat",proje * (*spotLightRefIter)->getPOVTransform());
			
			_stencilPass(projCamMat,camPos,screenSize,*spotLightRefIter);
			_lightPass(projCamMat,camPos,screenSize,*spotLightRefIter);
			++spotLightRefIter;
		}
		++spotLightIter;
	}
	lShader->disable();
} 

void NSGBuffer::draw(const NSMatrix4Df & proj, NSCamera * camera)
{
	use();
	glViewport(0, 0, w, h);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glCullFace(GL_BACK);
    glEnable(GL_CULL_FACE);

	drawGeometry(proj,camera);
	drawDeferred(proj,camera);
	disable();
	drawToScreenFBO();
}

void NSGBuffer::drawGeometry(const NSMatrix4Df & proj, NSCamera * camera)
{
	// Set the frame buffer draw buffers to the info textures
	GLenum drawBufs[TexNum];
    for (unsigned int i = 0 ; i < TexNum; ++i) 
		drawBufs[i] =  GL_COLOR_ATTACHMENT0 + i;
    glDrawBuffers(TexNum, drawBufs);

	glDepthMask(GL_TRUE);  // Must always come before clearing
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);
	glDisable(GL_BLEND);

	NSMatrix4Df camTrans = camera->getTransform();
	std::map<float, std::vector<std::pair<NSObjectReference*,NSShader*>>> depthSort;


	glClear(GL_STENCIL_BUFFER_BIT);
	glEnable(GL_STENCIL_TEST);
	glStencilFunc(GL_ALWAYS, 0, 0xFF);
	glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);

	NSObjectSet::Iterator iter = objects->begin();
	while (iter != objects->end())
	{
		if (iter->second.empty())
		{
			++iter;
			continue;
		}
		iter->first->enable();  // Enable the shader for this set of objects
		iter->first->setUniform("singleRefIndex", -1); // This sets picking so that it gets index from refID in shader
		std::vector<NSObject*>::iterator iter2 = iter->second.begin();
		while (iter2 != iter->second.end())
		{
			iter->first->setUniform("projCamMat",proj * camTrans);
			iter->first->setUniform("objectIndex", (*iter2)->getID() );
			if ((*iter2)->isHasTransluscentMaterial())
			{
				for (unsigned int i = 0; i < (*iter2)->getInstanceCount(); ++i)
				{
					NSObjectReference * ref =  mapArea->getReference(i,(*iter2));
					float currentDepth = ref->getDepth(camera);
					std::pair<NSObjectReference*,NSShader*> pair;
					pair.first = ref; pair.second = iter->first;
					depthSort[currentDepth].push_back(pair);
				}
				++iter2;
				continue;
			}
			(*iter2)->draw();
			++iter2;
		}
		iter->first->disable();
		++iter;
	}
	drawTranslucent(proj, camera, depthSort);
}

void NSGBuffer::drawDeferred(const NSMatrix4Df & proj, NSCamera * camera)
{
	enableTextures();
	NSMatrix4Df projCamMat(proj * camera->getTransform());
	// Set the target texture as final texture
    glDrawBuffer(Final);
    glClear(GL_COLOR_BUFFER_BIT); // clear final texture

	glDepthMask(GL_FALSE);
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_STENCIL_TEST);
	//glEnable(GL_DEPTH_TEST);
	glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
	glDisable(GL_BLEND);

	glCullFace(GL_FRONT);
	fsQuad->enable();
	fsQuad->setUniform("projCamMat",projCamMat);
	NSMatrix4Df trans;
	trans.translate(camera->getPosition(NSCamera::FREE)*-1.0f);
	trans.rotate(-90.0f,0.0f,0.0f);
	quad->drawSingle(trans,true);
	glCullFace(GL_BACK);

	
	glEnable(GL_STENCIL_TEST);
	glStencilFunc(GL_NOTEQUAL, 0, 0xFF);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	NSVec3Df camPos(camera->getPosition(NSCamera::FREE)*-1);
	NSVec2Df screenSize(w,h);
	mapArea->getDirectionalLight()->drawLight(projCamMat,camPos, screenSize, NULL);
	glDisable(GL_STENCIL_TEST);


    glEnable(GL_BLEND);
    glBlendEquation(GL_FUNC_ADD);
    glBlendFunc(GL_ONE, GL_ONE);

	blendPointLights(projCamMat,camPos,screenSize);
	blendSpotLights(projCamMat,camPos,screenSize,proj,camera);

	
}

void NSGBuffer::drawToScreenFBO()
{
	glDepthMask(GL_FALSE); // no need for adding to depth buffer
	glClear(GL_COLOR_BUFFER_BIT); // Clear frame buffer
	glDisable(GL_BLEND);
	glDisable(GL_DEPTH_TEST);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, FBO);
    glReadBuffer(Final); // set reading buffer to final texture
    glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR); // blit to main frame
    glReadBuffer(GL_NONE);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
}

void NSGBuffer::drawTranslucent(const NSMatrix4Df & proj, 
	NSCamera * camera, 
	std::map<float, std::vector<std::pair<NSObjectReference*,NSShader*>>> & depthSort)
{
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	// Now do a drawSingle operation on every item in the BST in reverse order so that the furthest
	// objects are drawn first and the closest ones are drawn last
	std::map<float,std::vector<std::pair<NSObjectReference*,NSShader*>>>::reverse_iterator sortIter = depthSort.rbegin();
	while (sortIter != depthSort.rend())
	{
		std::vector<std::pair<NSObjectReference*,NSShader*>>::iterator refIter = sortIter->second.begin();
		while (refIter != sortIter->second.end())
		{
			NSObject * base = refIter->first->getBase();  // Get the reference base
			NSShader * shader = refIter->second;          // And then get the shader in charge of drawing this reference
			unsigned int refID = refIter->first->getID();

			shader->enable(); // enable the shader and set the uniforms
			shader->setUniform("projCamMat",proj * camera->getTransform());
			shader->setUniform("objectIndex", base->getID() );
			shader->setUniform("singleRefIndex", (int)refID);
			base->drawSingle(refID);
			++refIter;
			shader->disable();
		}
		++sortIter;
	}
}

bool NSGBuffer::init(int width, int height)
{
	LogFile("Initializing GBuffer and shadow FBO");
	w = width; h = height;

	// Create the ShadowFBO
	glGenFramebuffers(1, &shadowFBO);
	glGenTextures(1, &shadowTexture);

    glBindTexture(GL_TEXTURE_2D, shadowTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 2048, 2048, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, shadowFBO);
	glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowTexture, 0);

    // Disable writes to the color buffer
    glDrawBuffer(GL_NONE);

	GLenum error = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (error != GL_FRAMEBUFFER_COMPLETE) 
	{
		std::stringstream ss;
		ss << "ERROR: Unable to create ShadowBuffer - Error code : " << error;
		LogFile l(ss);
        return false;
    }

	/* --------------------------------------------  Creating GBuffer --------------------------------------------------*/

    // Create the GBuffer FBO
    glGenFramebuffers(1, &FBO);
	// Create all textures
    glGenTextures(TexNum, textures);
	glGenTextures(1, &finalTexture);
    glGenTextures(1, &depthTexture);

	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, FBO);
	// Load textures for GBuf information
    for (unsigned int i = 0 ; i < TexNum; ++i) 
	{
		glBindTexture(GL_TEXTURE_2D, textures[i]);
		if (i == Pick)
		{
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32UI, w, h, 0, GL_RGBA_INTEGER, GL_UNSIGNED_INT, NULL);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		}
		else
		{
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, w, h, 0, GL_RGBA, GL_FLOAT, NULL);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		}
		glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, textures[i], 0);
    }
	// Load final texture
	glBindTexture(GL_TEXTURE_2D, finalTexture);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, w, h, 0, GL_RGBA, GL_FLOAT, NULL);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, Final, GL_TEXTURE_2D, finalTexture, 0);

    // Load depth texture
    glBindTexture(GL_TEXTURE_2D, depthTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH32F_STENCIL8, w, h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, Depth, GL_TEXTURE_2D, depthTexture, 0);
    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (status != GL_FRAMEBUFFER_COMPLETE) 
	{
		std::stringstream ss;
		ss << "ERROR: Unable to create GBuffer - Error code : " << status;
		LogFile l(ss);
        return false;
    }
	tex->use(NSTexture::CubeMap);
    // restore default FBO
	
    disable();

    return true;
}

bool NSGBuffer::resetSize(int width, int height)
{
	LogFile("Resetting GBuffer FBO");
    if (depthTexture != 0) 
	{
        glDeleteTextures(1, &depthTexture);
		depthTexture = 0;
		LogFile("Reset GBuffer depth texture");
    }
    if (finalTexture != 0) 
	{
        glDeleteTextures(1, &finalTexture);
		finalTexture = 0;
		LogFile("Reset GBuffer final texture");
    }
    if (shadowTexture != 0)
	{
        glDeleteTextures(1, &shadowTexture);
		shadowTexture = 0;
		LogFile("Reset shadow depth texture");
    }
    if (shadowFBO != 0)
	{
		glDeleteFramebuffers(1,&shadowFBO);
		shadowFBO = 0;
		LogFile("Reset shadowFBO");
    }
	for (int i = 0; i < TexNum; ++i)
	{
		if (textures[i] != 0) 
		{
			std::stringstream ss;
			glDeleteTextures(1, &textures[i]);
			textures[i] = 0;
			ss << "Reset GBuffer color texture " << i;
			LogFile l(ss);
		}
	}
	return NSFrameBufferObject::resetSize(width,height);
}

void NSGBuffer::enableTextures()
{
	// We don't need to bind the picking texture to any texture unit
	// Therefor TexNum should be one less
    for (unsigned int i = 0 ; i < TexNum - 1; i++) 
	{
		glActiveTexture(BASE_TEX_UNIT + NSTexture::GWorldPos + i);
        glBindTexture(GL_TEXTURE_2D, textures[i]);
    }
}

void NSGBuffer::enableShadowTexture()
{
	glActiveTexture(BASE_TEX_UNIT + NSTexture::Shadow);
	glBindTexture(GL_TEXTURE_2D,shadowTexture);
}

NSGBuffer::ObjectIndex NSGBuffer::getObjectIndex(int mouseX, int mouseY)
{
    glBindFramebuffer(GL_READ_FRAMEBUFFER, FBO);
    glReadBuffer(GL_COLOR_ATTACHMENT0 + Pick);
    ObjectIndex index;
    glReadPixels(mouseX, mouseY, 1, 1, GL_RGBA_INTEGER, GL_UNSIGNED_INT, &index);
	GLenum error = glGetError();
	if (error!=0)
	{
		std::stringstream ss;
		ss << "Error with reading from frame buffer.. Error code: " << error;
		LogFile l(ss);
	}
    glReadBuffer(GL_NONE);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
    return index;
}

void NSGBuffer::useShadow()
{
	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, shadowFBO);
}

NSGBuffer::ObjectIndex::ObjectIndex():objectIndex(0),referenceIndex(0),primIndex(0)
{}